<?php
/**    
  *
  * Copyright (c) 2009, Persistent Systems Limited
  *
  * Redistribution and use, with or without modification, are permitted 
  *  provided that the following  conditions are met:
  *   - Redistributions of source code must retain the above copyright notice, 
  *     this list of conditions and the following disclaimer.
  *   - Neither the name of Persistent Systems Limited nor the names of its contributors 
  *     may be used to endorse or promote products derived from this software 
  *     without specific prior written permission.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
  
/**
  *
  * This file contains the definition of SaveResult class.
  */

class SaveResult
{
    /**
     * Reference to contextobject instance
     * Type: ObjectContext
     */
    protected $_context;	
    /**
     * The list holding the merged result of values in ObjectToContext and
     * Bindings dictionaries of context object, only the entries with State
     * Unchanged will be loaded. For each entries in this list one changeset
     * (a MIME part of batchRequest) will be genereated.
     * type: array of Entry (see Entry class)
     */
    protected $_changedEntries;	
    /**
     * The string holding batch boundary for the batch request
     * format will be batch_{guid}
     * type: string
     */
    protected $_batchBoundary;	
    /**
     * The string holding changeset boundary for the batch request
     * format will be changeset_{guid}
     * type: string
     */
    protected $_changesetBoundry;
    /**
     * The string holding the body part of batch request. This variable
     * type: string
     */
    protected $_batchRequestBody;
    /**
     * The string holding the header part of batch request
     * type: string
     */
    protected $_btachRequestHeader;
    /**
     * The list holding the collection of http response for each chnageset
     * in the batch request.
     * type: array of HttpResponse (see HttpResonse Class)
     */
    protected $_httpResponses;
    
    /**
     * Constructs a new SaveResult Object.
     *  @param ObjectContext $context The context object.
     */
    public function SaveResult($context)
    {		
        $this->_context = $context;
        $mergedDictionary = Dictionary::Merge($this->_context->ObjectToResource, 
                                        $this->_context->Bindings, 
                                        "State", Entitystates::Unchanged,
                                        FALSE);
        $mergedDictionary->Sort("ChangeOrder");
        $this->_changedEntries =  $mergedDictionary->Values();
        $this->_batchBoundary = "batch_" . Guid::NewGuid();
    }

    /*
     * This function will generates the batchrequest from the $_changedEntries
     * list
     */
    public function BatchRequest()
    {		
        $this->_changesetBoundry = 	"changeset_" . Guid::NewGuid();
        $changedEntriesCount = count($this->_changedEntries);
        if($changedEntriesCount > 0)
        {
            Utility::WriteLine($this->_batchRequestBody , "--" . $this->_batchBoundary);
            Utility::WriteLine($this->_batchRequestBody,  "Content-Type: multipart/mixed; boundary=" . $this->_changesetBoundry);
            Utility::WriteLine($this->_batchRequestBody, null);
            for($i = 0; $i < $changedEntriesCount; $i++)
            {
                Utility::WriteLine($this->_batchRequestBody, "--" . $this->_changesetBoundry);
                $changesetHeader = $this->CreateChangeSetHeader($i);
                $changesetBody =$this->CreateChangeSetBody($i);
                if ($changesetBody != null)
                {
                    Utility::WriteLine($changesetHeader, "Content-Length: " . strlen($changesetBody));
                }
                Utility::WriteLine($changesetHeader, null);
                $this->_batchRequestBody = $this->_batchRequestBody . $changesetHeader;
                if($changesetBody != null)
                {
                    $this->_batchRequestBody = $this->_batchRequestBody . $changesetBody;
                }
            }
            Utility::WriteLine($this->_batchRequestBody, "--" . $this->_changesetBoundry . "--");
            Utility::WriteLine($this->_batchRequestBody, "--" . $this->_batchBoundary . "--");
            $this->PerformBatchRequest();			
            $this->EndBatchRequest();
        }
    }

    /**
     * This function will fire the batch request and load _httpResponses with response
     * belongs to each changeset in the batch request.
     */
    protected function PerformBatchRequest()
    {
        $uri = $this->_context->GetBaseUri();
        $httpBatchRequest = new HttpBatchRequest($uri, 
                                                 $this->_batchBoundary, 
                                                 $this->_batchRequestBody,
                                                 $this->_context->Credential,
                                                 $this->_context->HttpProxy,
                                                 $this->_context->CustomHeaders);
        $httpBatchResponse = $httpBatchRequest->GetResponse();
        //Error check for batch Response ex:UnAuthorized
        if(! $httpBatchResponse->IsError())
        {		
            $this->_httpResponses = $httpBatchResponse->GetSubBatchHttpResponses();	
        }
        else
        {
            throw new ADODotNETDataServicesException("HTTP Status Code: " . $httpBatchResponse->GetCode() . 
                                                     " HTTP Message: " . $httpBatchResponse->GetMessage());
        }
    }

    /*
     *  This function will:
     *  a. Checks any error is returned by dataservice (ex: if user try to add
     *          a record with existing key)
     *  b. Populate the entities created by user from the response
     *  c. Cleanup activity
     */
    protected function EndBatchRequest()
    {
        $this->CheckForDataServiceVersion();
        $this->CheckForDataServiceError();
        $this->LoadResourceBoxes();
        $this->_context->Bindings->RemoveAll();
        $resourceBoxes = $this->_context->ObjectToResource->Values();
        foreach($resourceBoxes as $resourceBox)
        {
            if($resourceBox->State == EntityStates::Deleted)
            {
                if(null != $resourceBox->Identity)
                {
                    unset($this->_context->IdentityToResource[$resourceBox->Identity]);
                }
                $this->_context->ObjectToResource->Remove($resourceBox->GetResource());
            }
            if($resourceBox->State == EntityStates::Modified)
            {
                $resourceBox->State = EntityStates::Unchanged;
            }
        }
    }
    
    /*
     * Check the version of responses.
     */
    protected function CheckForDataServiceVersion()
    {
        foreach($this->_httpResponses as $httpResponse)
        {
            $headers = $httpResponse->getHeaders();
            if(isset($headers['Dataserviceversion']) && 
            ((int)$headers['Dataserviceversion'] > (int)Resource::DataServiceVersion))
            {
                throw new ADODotNETDataServicesException(Resource::VersionMisMatch . $headers['Dataserviceversion']);
            }		
        }
    }
    
    /*
     * Checks resposnce from data service contains any error if so
     * throw exception
     */
    protected function CheckForDataServiceError()
    {
        if(count($this->_httpResponses) == 1)
        {
            $httpResponse = $this->_httpResponses[0];
            if($httpResponse->isError())
            {
                $headers = $httpResponse->getHeaders();
                $content_type = isset($headers['Content-type']) ? $headers['Content-type']: "";
                throw new ADODotNETDataServicesException($httpResponse->getBody(), $content_type);
            }
        }
    }

    /**
     * Load the resource boxes holding the resources added by user from
     * corrosponding response from server.
     * @return <type> no return value
     */
    protected function LoadResourceBoxes()
    {
        foreach($this->_changedEntries as $changedEntry)
        {
            if(($changedEntry->IsResource()) &&
               ($changedEntry->State == EntityStates::Added))
            {
                $this->LoadResourceBox($changedEntry);
            }
        }
    }

    /**
     *
     * @param <type> $resourceBox
     * @return <type> no return value
     */
    protected function LoadResourceBox($resourceBox)
    {
         $Content_ID = $resourceBox->ChangeOrder;		
         $str = $this->GetBodyByContentID($Content_ID, $content_type);		 
         if($str == null)
         {
            return;
         }	
        $this->_context->LoadResourceBox($str, $resourceBox, $content_type);
        $resourceBox->State = EntityStates::Unchanged;
    }

    /**
     *
     * @param <type> $Content_ID
     * @return <type> json response
     * This function will return body (json format)of httpResponse with
     * Content-id header equal to $Content_ID
     */
    protected function GetBodyByContentID($Content_ID, &$content_type)
    {
        foreach($this->_httpResponses as $httpResponse)
        {
            $headers = $httpResponse->getHeaders();
            if($headers['Content-id'] == $Content_ID)
            {
                if(isset($headers['Content-type']))
                {
                    $content_type = $headers['Content-type'];
                } 
                return $httpResponse->getBody();
            }
        }
        return null;
    }

    /**
     *
     * @param <integer> $index
     * @return <string> returns body part of one MIME part
     * This fuction will creates changeset body (MIME part body) for
     * a changeset that will become a part batchrequest.
     */
    protected function CreateChangeSetBody($index)
    {
        $changesetBody;
        $entry = $this->_changedEntries[$index];
        if($entry->IsResource())
        {		
            $changesetBody = $this->CreateChangeSetBodyForResource($entry, true);
        }
        else
        {
            $changesetBody = $this->CreateChangesetBodyForBinding($entry, true);
        }
        return $changesetBody;
    }
    /**
     *
     * @param <ResourceBox> $resourceBox
     * @param <bool> $newline
     * @return <string> Body of changeset for entity in json format
     */
    protected function CreateChangeSetBodyForResource($resourceBox, $newline)
    {		
        if(EntityStates::Deleted == $resourceBox->State)
        {
            return null;
        }
        if(EntityStates::Added != $resourceBox->State &&
            EntityStates::Modified != $resourceBox->State)
        {
            throw new Exception("Unexpected Entity State while trying to generate changeset body for resource");
        }
        
        $changesetBodyForResource = null;
                
        if(strcmp($this->_context->Content_Type, Resource::Content_Type_JSON) == 0)
        {			
            $changesetBodyForResource = $this->CreateChangeSetBodyForResourceInJSON($resourceBox, $newline);
        }
        else
        {
            $changesetBodyForResource = $this->CreateChangeSetBodyForResourceInATOM($resourceBox, false);
        }
        
        return $changesetBodyForResource;
    }
    
    protected function CreateChangeSetBodyForResourceInJSON($resourceBox, $newline)
    {
        $changesetBodyForResource = null;
        $object = $resourceBox->getResource();
        $entityName = get_class($object);
        $changesetBodyForResource = "{ ";
        $PropertyArray = Array();
        Utility::getProperties($entityName, $PropertyArray);
        foreach ($PropertyArray as $ProperyName) 
        {
            $prop = new ReflectionProperty($object, $ProperyName);
            if ($prop->isPublic()) 
            {
                if (Utility::ContainAttribute($prop->getDocComment(), 'EntityProperty')) 
                {
                    if ($prop->getValue($object) == '') 
                    {
                        continue;
                    }
                    $changesetBodyForResource = $changesetBodyForResource . $ProperyName;
                    $changesetBodyForResource = $changesetBodyForResource . ":'";
                    $propertyValue = Utility::edmToJSONType($prop, $object);
                    $changesetBodyForResource = $changesetBodyForResource . $propertyValue;
                    $changesetBodyForResource = $changesetBodyForResource . "',";
                }
            }
        }
        $changesetBodyForResource[strlen($changesetBodyForResource)-1] = '}';
        if($newline)
        {
            Utility::WriteLine($changesetBodyForResource, null);
        }
        return $changesetBodyForResource;		
    }
    
    protected function CreateChangeSetBodyForResourceInATOM($resourceBox, $newline)
    {
        $changesetBodyForResource = null;
        $object = $resourceBox->getResource();			
        $entityName = get_class($object);
        Utility::WriteLine($changesetBodyForResource, "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>");
        Utility::WriteLine($changesetBodyForResource, "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\">");
        Utility::WriteLine($changesetBodyForResource, "<title />");
        Utility::WriteLine($changesetBodyForResource, "<author>");
        Utility::WriteLine($changesetBodyForResource, "<name />");
        Utility::WriteLine($changesetBodyForResource, "</author>");
        if( EntityStates::Modified == $resourceBox->State)
        {
            $editLinkUri = $this->GenerateEditLinkUri($this->_context->GetBaseUriWithSlash(),
                                                  $resourceBox->GetResource(),
                                                  FALSE);	
            Utility::WriteLine($changesetBodyForResource, "<id>" . $editLinkUri . "</id>" );	
        }
        else
        {
            Utility::WriteLine($changesetBodyForResource, "<id />");
        }
        
        Utility::WriteLine($changesetBodyForResource, "<content type=\"application/xml\">");
        Utility::WriteLine($changesetBodyForResource, "<m:properties>");
        
        $PropertyArray = Array();        
        Utility::getProperties($entityName, $PropertyArray);
        foreach ($PropertyArray as $ProperyName) 
        {
            $prop = new ReflectionProperty($object, $ProperyName);
            if ($prop->isPublic()) 
            {
                if (Utility::ContainAttribute($prop->getDocComment(), 'EntityProperty')) 
                {
                    $propertyValue = $prop->getValue($object);
                    
                    $type = Utility::GetPropertyType($prop, $notNullable);
                    
                    //While making the property node we can either specify
                    //<d:Freight m:type="Edm.Decimal" m:null="true" />
                    //or <d:Freight m:null="true" />
                    //i.e specifying data type is not mandatory
                    
                    $property = "";
                    if ($propertyValue == '') 
                    {
                        if(!$notNullable)
                        {
                            $property = "<d:" . $ProperyName . " " . "m:null=\"true\" />";
                        }
                        else
                        {
                            //ex: In the case of OrderID, the ID is a autonumber, so user will not
                            //specify this value, since its a nonnullable value we cant set null=true
                            //property, in this case the correct property node should be
                            //<d:OrderID">0</d:OrderID">, but instead of this we can also achieve the
                            //same effect by not adding the property itself in xml
                            
                            continue;
                        }
                    }
                    else
                    {
                        $property = "<d:" . $ProperyName . ">" . $propertyValue . "</d:" . $ProperyName . ">";
                    }
                    
                    Utility::WriteLine($changesetBodyForResource, $property);
                }
            }
        }
        
        Utility::WriteLine($changesetBodyForResource, "</m:properties>");
        Utility::WriteLine($changesetBodyForResource, "</content>");
        Utility::WriteLine($changesetBodyForResource, "</entry>");
        
        if($newline)
        {
            Utility::WriteLine($changesetBodyForResource, null);
        }
        return $changesetBodyForResource;	
    }
    
    /**
     *
     * @param <RelatedEnd> $binding
     * @param <bool> $newline
     * @return <string> Body of changeset for binding
     */
    protected function CreateChangesetBodyForBinding($binding, $newline)
    {
        if ((EntityStates::Added != $binding->State) && 
            (EntityStates::Modified !=  $binding->State))
        {
            return null;
        }
        $changesetBodyForBinding = null;
        $targetObjectUri = null;
        $targetResourcebox = null;		
        $this->_context->ObjectToResource->TryGetValue($binding->GetTargetResource(), $targetResourcebox);		
        if($targetResourcebox->Identity != null)
        {
            $targetObjectUri = $targetResourcebox->Identity;
        }
        else
        {
            $targetObjectUri = "$" . $targetResourcebox->ChangeOrder;
        }
        
        Utility::WriteLine($changesetBodyForBinding, "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>");
        $changesetBodyForBinding = $changesetBodyForBinding . "<uri xmlns=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\">";
        $changesetBodyForBinding = $changesetBodyForBinding . $targetObjectUri . "</uri>";
        if($newline)
        {
            Utility::WriteLine($changesetBodyForBinding, null);
        }
        
        return $changesetBodyForBinding;
    }

    /**
     *
     * @param <integer> $index
     * @return <string> returns header part of one MIME part
     * This fuction will creates changeset header (MIME part header) for
     * a changeset that will become a part batchrequest.
     */
    public function CreateChangeSetHeader($index)
    {
        $changesetHeader = null;
        $entry = $this->_changedEntries[$index];
        if($entry->IsResource())
        {		
            $changesetHeader = $this->CreateChangeSetHeaderForResource($entry, true);
        }
        else
        {
            $changesetHeader = $this->CreateChangesetHeaderForBinding($entry, true);
        }
        return $changesetHeader;
    }

    /**
     *
     * @param <ResourceBox> $resourceBox
     * @return <string> returns header of changeset for an entity in json format
     */
    public function CreateChangeSetHeaderForResource($resourceBox)
    {
         $changesetHeaderForResource = null;
         $resourceUri = $resourceBox->GetResourceUri($this->_context->GetBaseUriWithSlash());
         $this->WriteOperationRequestHeaders($changesetHeaderForResource, 
                                      $this->GetEntityHttpMethod($resourceBox->State), 
                                      $resourceUri);
         Utility::WriteLine($changesetHeaderForResource, "Content-ID: " . $resourceBox->ChangeOrder);
         Utility::WriteLine($changesetHeaderForResource, "Accept: " . $this->_context->Accept);
         
         if (EntityStates::Deleted != $resourceBox->State)
         {
            if(strcmp($this->_context->Content_Type, Resource::Content_Type_JSON) == 0)
            {
                Utility::WriteLine($changesetHeaderForResource, "Content-Type: " . "application/json" . ";" . "type=entry");
            }
            else
            {			
                Utility::WriteLine($changesetHeaderForResource, "Content-Type: " . "application/atom+xml" . ";" . "type=entry");
            }
         }
     
         return $changesetHeaderForResource;

    }

    /**
     *
     * @param <RelatedEnd> $binding
     * @return <string> returns header of changeset for binding in json format
     */
    protected function CreateChangesetHeaderForBinding($binding)
    {		
        $changesetHeaderForBinding = null;
        $uri = $this->CreateRequestRelativeUri($binding);
        $sourceResourceBox = null;
        $this->_context->ObjectToResource->TryGetValue($binding->GetSourceResource(), $sourceResourceBox);
        $absoluteUri = null;
        if (null != $sourceResourceBox->Identity)
        {
            $absoluteUri = $sourceResourceBox->GetResourceUri($this->_context->GetBaseUriWithSlash());
        }
        else
        {
            $absoluteUri = "$" . $sourceResourceBox->ChangeOrder;
        }
        $absoluteUri = $absoluteUri . "/" . $uri;
        $this->WriteOperationRequestHeaders($changesetHeaderForBinding, 
                                            $this->GetBindingHttpMethod($binding->State), 
                                            $absoluteUri);
        
        Utility::WriteLine($changesetHeaderForBinding, "DataServiceVersion: 1.0;NetFx");
        Utility::WriteLine($changesetHeaderForBinding, "Accept: " . $this->_context->Accept);
        Utility::WriteLine($changesetHeaderForBinding, "Content-ID: " . $binding->ChangeOrder);
        if ((EntityStates::Added == $binding->State) || 
            (EntityStates::Modified == $binding->State))
        {
            Utility::WriteLine($changesetHeaderForBinding, "Content-Type: application/xml");
        }
        return $changesetHeaderForBinding;
    }
    
    protected function CreateRequestRelativeUri($binding)
    {
        if (EntityStates::Added != $binding->State)
        {
            $targetResourceBox = null;
            $this->_context->ObjectToResource->TryGetValue($binding->GetTargetResource(), $targetResourceBox);
            $editLinkUri = $this->GenerateEditLinkUri($this->_context->GetBaseUriWithSlash(),
                                                  $targetResourceBox->GetResource(),
                                                  TRUE);
            return "$" . "links/" . $editLinkUri;
        }
        return "$" . "links/" . $binding->GetSourceProperty();
    }

    protected function GenerateEditLinkUri($baseUriWithSlash, $resource, $isRelative)
    {
        $editLinkUri = '';
        if(!$isRelative)
        {
            $editLinkUri = $baseUriWithSlash;
        }		
        $editLinkUri = $editLinkUri . Utility::getUri($resource);
        return $editLinkUri;
    }
    
    protected function WriteOperationRequestHeaders(&$outVal, $methodName, $uri)
    {
        Utility::WriteLine($outVal, "Content-Type: application/http");
        Utility::WriteLine($outVal, "Content-Transfer-Encoding: binary");
        Utility::WriteLine($outVal, null);
        Utility::WriteLine($outVal, $methodName . " " . $uri . " " . "HTTP/1.1");
    }

    /**
     *
     * @param <EntityStates> $state
     * @return <string> Returns HTTP method to be used for an entity
     * based on the state of the ResourceBox holding the entity
     */
    protected function GetEntityHttpMethod($state)
    {
        if($state == EntityStates::Added)
        {
            return "POST";
        }
        
        if($state == EntityStates::Deleted)
        {
            return "DELETE";
        }
        
        if($state == EntityStates::Modified)
        {
            return "MERGE";
        }
        
        throw new Exception("Invalid entity state while generating HTTP method");
    }

    /**
     *
     * @param <EntityStates> $state
     * @return <string> Returns HTTP method to be used for a binding
     * based on the state of the RelatedEnd holding the bindning
     */
    public function GetBindingHttpMethod($state)
    {
        if (EntityStates::Deleted == $state)
        {
            return "DELETE";
        }
        return "POST";
    }

    
}
?>